package com.hmdp.service.impl;

import com.fasterxml.jackson.databind.JsonSerializer;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.exception.VoucherException;
import com.hmdp.lock.GaiJinRedisLock;
import com.hmdp.lock.LuaRedisLock;
import com.hmdp.lock.SimpleRedisLock;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    private static final String LUA_SCRIPT_NAME = "/lua/secKill2.lua";
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource(LUA_SCRIPT_NAME));
        SECKILL_SCRIPT.setResultType(Long.class);
    }

    @Resource
    private RedissonClient redissonClient;
    public Long  seckillVoucher(Long voucherId) {
//        return seckillVoucherYiBU(voucherId);
        return seckillVoucherJiQun3(voucherId);
    }


    @Override
    @Transactional
    public Long createVoucherOrder(Long voucherId) {
        // 一人一单
        Long userId = UserHolder.getUser().getId();
        if(query().eq("user_id",userId).eq("voucher_id",voucherId).count()>0){
            throw new VoucherException("您已经购买过该优惠券");
        }
        // 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock=stock-1")
                .eq("voucher_id", voucherId).gt("stock",0) // 乐观锁，解决超卖
                .update();
        if(!success){
            throw new VoucherException("库存不足");
        }
        // 创建订单
        VoucherOrder order = new VoucherOrder();
        // 全局唯一ID
        long orderId = redisIdWorker.nextId("order");
        order.setId(orderId);
        order.setUserId(UserHolder.getUser().getId());
        order.setVoucherId(voucherId);
        save(order);
        return orderId;
    }

    @Transactional
    public Long seckillVoucherByLeGuan(Long voucherId) {
        /**
         * 乐观锁：
         * 版本号法，更新时判断之前的版本号是否正确，不正确则返回错误，正确则更新库存，并返回更新后的版本号
         * 根据实际情况选择版本号字段，比如stock就可以作为一种版本
         * 解决超卖，.gt("stock",0)即可
         *
         * */
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if(voucher.getBeginTime().isAfter(LocalDateTime.now())){
            throw new VoucherException("秒杀尚未开始");
        }
        if(voucher.getEndTime().isBefore(LocalDateTime.now())){
            throw new VoucherException("秒杀已经结束");
        }
        if(voucher.getStock()<=0){
            throw new VoucherException("库存不足");
        }
        // 扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock=stock-1")
                .eq("voucher_id", voucherId).gt("stock",0) // 乐观锁，解决超卖
                .update();
        if(!success){
            throw new VoucherException("库存不足");
        }
        // 创建订单
        VoucherOrder order = new VoucherOrder();
        // 全局唯一ID
        long orderId = redisIdWorker.nextId("order");
        order.setId(orderId);
        order.setUserId(UserHolder.getUser().getId());
        order.setVoucherId(voucherId);
        save(order);
        return orderId;
    }



    public Long seckillVoucherByYiRenYiDan(Long voucherId) {
        /**
         *
         * 乐观锁：
         * 版本号法，更新时判断之前的版本号是否正确，不正确则返回错误，正确则更新库存，并返回更新后的版本号
         * 根据实际情况选择版本号字段，比如stock就可以作为一种版本
         * 解决超卖，.gt("stock",0)即可
         *
         * */
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if(voucher.getBeginTime().isAfter(LocalDateTime.now())){
            throw new VoucherException("秒杀尚未开始");
        }
        if(voucher.getEndTime().isBefore(LocalDateTime.now())){
            throw new VoucherException("秒杀已经结束");
        }
        if(voucher.getStock()<=0){
            throw new VoucherException("库存不足");
        }

        Long userId = UserHolder.getUser().getId();
        /**
         * 加锁时机：
         * 1. 通过userID进行加锁，但userId.toString,每次都创建新一个对象，导致锁不住，用intern()解决，intern返回规范表示，从常量池查找对应的地址
         * 2.事务提交后再释放锁
         * 如果在createVoucherOrder里面加锁，函数结束后，锁释放，但是事务还没有提交，存在并发安全问题。
         * 所以必须在事务提交后释放锁， 锁整个函数
         * 3.事务要生效是对当前的类做了动态代理，拿到代理对象，通过代理对象this.createVoucherOrder(voucherId);
         * this是非代理对象，没有事务功能
         * 添加注解
         * <dependency>
         *  <groupId>org.aspectj</groupId>
         *  <artifactId>aspectjweaver</artifactId>
         * </dependency>
         * 启动类暴露代理
         * @EnableAspectJAutoProxy(exposeProxy = true)
         * */
//        synchronized (
//                userId.toString().intern()
//        ){
//            Long orderId = this.createVoucherOrder(voucherId);
//            return orderId;
//        }
        synchronized (
                userId.toString().intern()
        ){
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            Long orderId = proxy.createVoucherOrder(voucherId);
            return orderId;
        }

    }

    // 解决集群问题
    public Long seckillVoucherJiQun(Long voucherId) {
        /**
         *
         * 乐观锁：
         * 版本号法，更新时判断之前的版本号是否正确，不正确则返回错误，正确则更新库存，并返回更新后的版本号
         * 根据实际情况选择版本号字段，比如stock就可以作为一种版本
         * 解决超卖，.gt("stock",0)即可
         *
         * */
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            throw new VoucherException("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            throw new VoucherException("秒杀已经结束");
        }
        if (voucher.getStock() <= 0) {
            throw new VoucherException("库存不足");
        }

        Long userId = UserHolder.getUser().getId();
        /**
         * 加锁时机：
         * 1. 通过userID进行加锁，但userId.toString,每次都创建新一个对象，导致锁不住，用intern()解决，intern返回规范表示，从常量池查找对应的地址
         * 2.事务提交后再释放锁
         * 如果在createVoucherOrder里面加锁，函数结束后，锁释放，但是事务还没有提交，存在并发安全问题。
         * 所以必须在事务提交后释放锁， 锁整个函数
         * 3.事务要生效是对当前的类做了动态代理，拿到代理对象，通过代理对象this.createVoucherOrder(voucherId);
         * this是非代理对象，没有事务功能
         * 添加注解
         * <dependency>
         *  <groupId>org.aspectj</groupId>
         *  <artifactId>aspectjweaver</artifactId>
         * </dependency>
         * 启动类暴露代理
         * @EnableAspectJAutoProxy(exposeProxy = true)
         * */
//        synchronized (
//                userId.toString().intern()
//        ){
//            Long orderId = this.createVoucherOrder(voucherId);
//            return orderId;
//        }
        // 创建锁对象
        String lockKey = "order:" + userId;
        SimpleRedisLock lock = new SimpleRedisLock(lockKey, stringRedisTemplate);
        boolean isLock = lock.tryLock(1200);
        if (!isLock) {
            throw new VoucherException("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy  = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    // 判断锁的标识再释放时是否一致
    public Long seckillVoucherJiQun2(Long voucherId) {
        /**
         *
         * 乐观锁：
         * 版本号法，更新时判断之前的版本号是否正确，不正确则返回错误，正确则更新库存，并返回更新后的版本号
         * 根据实际情况选择版本号字段，比如stock就可以作为一种版本
         * 解决超卖，.gt("stock",0)即可
         *
         * */
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            throw new VoucherException("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            throw new VoucherException("秒杀已经结束");
        }
        if (voucher.getStock() <= 0) {
            throw new VoucherException("库存不足");
        }

        Long userId = UserHolder.getUser().getId();
        /**
         * 加锁时机：
         * 1. 通过userID进行加锁，但userId.toString,每次都创建新一个对象，导致锁不住，用intern()解决，intern返回规范表示，从常量池查找对应的地址
         * 2.事务提交后再释放锁
         * 如果在createVoucherOrder里面加锁，函数结束后，锁释放，但是事务还没有提交，存在并发安全问题。
         * 所以必须在事务提交后释放锁， 锁整个函数
         * 3.事务要生效是对当前的类做了动态代理，拿到代理对象，通过代理对象this.createVoucherOrder(voucherId);
         * this是非代理对象，没有事务功能
         * 添加注解
         * <dependency>
         *  <groupId>org.aspectj</groupId>
         *  <artifactId>aspectjweaver</artifactId>
         * </dependency>
         * 启动类暴露代理
         * @EnableAspectJAutoProxy(exposeProxy = true)
         * */
//        synchronized (
//                userId.toString().intern()
//        ){
//            Long orderId = this.createVoucherOrder(voucherId);
//            return orderId;
//        }
        // 创建锁对象
        String lockKey = "order:" + userId;
        GaiJinRedisLock lock = new GaiJinRedisLock(lockKey, stringRedisTemplate);
        boolean isLock = lock.tryLock(1200);
        if (!isLock) {
            throw new VoucherException("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy  = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

     public Long seckillVoucherJiQun3(Long voucherId) {
        /**
         *
         * 乐观锁：
         * 版本号法，更新时判断之前的版本号是否正确，不正确则返回错误，正确则更新库存，并返回更新后的版本号
         * 根据实际情况选择版本号字段，比如stock就可以作为一种版本
         * 解决超卖，.gt("stock",0)即可
         *
         * */
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            throw new VoucherException("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            throw new VoucherException("秒杀已经结束");
        }
        if (voucher.getStock() <= 0) {
            throw new VoucherException("库存不足");
        }

        Long userId = UserHolder.getUser().getId();
        /**
         * 加锁时机：
         * 1. 通过userID进行加锁，但userId.toString,每次都创建新一个对象，导致锁不住，用intern()解决，intern返回规范表示，从常量池查找对应的地址
         * 2.事务提交后再释放锁
         * 如果在createVoucherOrder里面加锁，函数结束后，锁释放，但是事务还没有提交，存在并发安全问题。
         * 所以必须在事务提交后释放锁， 锁整个函数
         * 3.事务要生效是对当前的类做了动态代理，拿到代理对象，通过代理对象this.createVoucherOrder(voucherId);
         * this是非代理对象，没有事务功能
         * 添加注解
         * <dependency>
         *  <groupId>org.aspectj</groupId>
         *  <artifactId>aspectjweaver</artifactId>
         * </dependency>
         * 启动类暴露代理
         * @EnableAspectJAutoProxy(exposeProxy = true)
         * */
//        synchronized (
//                userId.toString().intern()
//        ){
//            Long orderId = this.createVoucherOrder(voucherId);
//            return orderId;
//        }
        // 创建锁对象
        String lockKey = "order:" + userId;
        LuaRedisLock lock = new LuaRedisLock(lockKey, stringRedisTemplate);
        boolean isLock = lock.tryLock(10);
        if (!isLock) {
            throw new VoucherException("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy  = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    /***
     * @description 用redisson实现
     * @param  : voucherId - [java.lang.Long]
     * @return : java.lang.Long
     **/
    public Long seckillVoucherRedisson(Long voucherId) throws InterruptedException {
        /**
         *
         * 乐观锁：
         * 版本号法，更新时判断之前的版本号是否正确，不正确则返回错误，正确则更新库存，并返回更新后的版本号
         * 根据实际情况选择版本号字段，比如stock就可以作为一种版本
         * 解决超卖，.gt("stock",0)即可
         *
         * */
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            throw new VoucherException("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            throw new VoucherException("秒杀已经结束");
        }
        if (voucher.getStock() <= 0) {
            throw new VoucherException("库存不足");
        }

        Long userId = UserHolder.getUser().getId();
        /**
         * 加锁时机：
         * 1. 通过userID进行加锁，但userId.toString,每次都创建新一个对象，导致锁不住，用intern()解决，intern返回规范表示，从常量池查找对应的地址
         * 2.事务提交后再释放锁
         * 如果在createVoucherOrder里面加锁，函数结束后，锁释放，但是事务还没有提交，存在并发安全问题。
         * 所以必须在事务提交后释放锁， 锁整个函数
         * 3.事务要生效是对当前的类做了动态代理，拿到代理对象，通过代理对象this.createVoucherOrder(voucherId);
         * this是非代理对象，没有事务功能
         * 添加注解
         * <dependency>
         *  <groupId>org.aspectj</groupId>
         *  <artifactId>aspectjweaver</artifactId>
         * </dependency>
         * 启动类暴露代理
         * @EnableAspectJAutoProxy(exposeProxy = true)
         * */
//        synchronized (
//                userId.toString().intern()
//        ){
//            Long orderId = this.createVoucherOrder(voucherId);
//            return orderId;
//        }
        // 创建锁对象
        String lockKey = "order:" + userId;
        RLock lock = redissonClient.getLock(lockKey);
        boolean isLock = lock.tryLock(1L, 10L, TimeUnit.SECONDS);
        if (!isLock) {
            throw new VoucherException("不允许重复下单");
        }
        try {
            IVoucherOrderService proxy  = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }



    /**
     * @description redis优化，将查询库存和是否有订单存在，放到LUA脚本中，减少数据库访问次数
     * @author: sats@jz
     * @param  : voucherId - [java.lang.Long]
     * @return : java.lang.Long
     **/
    public Long seckillVoucherYiBU(Long voucherId){
        // 执行LUA脚本
        Long userId = UserHolder.getUser().getId();
        // 当前时间的时间戳
        Long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),
                userId.toString(),
                now.toString()
        );
        int r = result.intValue();
        if(r == 1){
            throw new VoucherException("库存不足");
        } else if(r == 2){
            throw new VoucherException("不能重复下单");
        } else if(r == 3){
            throw new VoucherException("活动未开始");
        } else if(r == 4){
            throw new VoucherException("活动已结束");
        }
        // 有购买资格，保存到阻塞队列
        long orderId = redisIdWorker.nextId("seckill:orderID");

        // 加入异步MQ队列,实现订单创建
        VoucherOrder order = new VoucherOrder();
        order.setId(orderId);
        order.setUserId(UserHolder.getUser().getId());
        order.setVoucherId(voucherId);
//        rabbitTemplate.convertAndSend("order.direct", "order.routing.key", order);
        rabbitTemplate.convertAndSend("order.exchange", "order.seckill.routing.key", order);
        return orderId;

    }

}
